package com.andrewchatham.pony;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;

import android.net.DhcpInfo;
import android.net.wifi.WifiManager;
import android.util.Log;

import com.andrewchatham.SynctasticProto.AnnounceRequest;
import com.andrewchatham.SynctasticProto.AnnounceResponse;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * Code for dealing with remote server discovery. 
 * This class tries to send a broadcast UDP packet over your wifi network to
 * discover the remote sync service.
 */

public class Discoverer extends Thread {
  private static final String TAG = "Discovery";
  private static final int DISCOVERY_PORT = 28992;
  private static final int TIMEOUT_MS = 500;
  private static final byte[] VALID_RESPONSE = "PONIES ARE AWESOME".getBytes();
  private Receiver mReceiver;

  private WifiManager mWifi;

  interface Receiver {
    /**
     * Process the list of discovered servers. This is always called once after
     * a short timeout.
     * 
     * @param servers
     *          list of discovered servers, null on error
     */
    void addAnnouncedServers(ArrayList<AnnounceResponse> servers);
  }

  Discoverer(WifiManager wifi, Receiver receiver) {
    mWifi = wifi;
    mReceiver = receiver;
  }

  public void run() {
    ArrayList<AnnounceResponse> servers = null;
    try {
      DatagramSocket socket = new DatagramSocket();
      socket.setBroadcast(true);
      socket.setSoTimeout(TIMEOUT_MS);

      sendDiscoveryRequest(socket);
      servers = listenForResponses(socket);
      socket.close();
    } catch (IOException e) {
      servers = new ArrayList<AnnounceResponse>();  // use an empty one
      Log.e(TAG, "Could not send discovery request", e);
    }
    mReceiver.addAnnouncedServers(servers);
  }

  /**
   * Send a broadcast UDP packet containing a request for sync services to
   * announce themselves.
   * 
   * @throws IOException
   */
  private void sendDiscoveryRequest(DatagramSocket socket) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    out.write("I LOVE PONIES".getBytes());
    AnnounceRequest req = AnnounceRequest.newBuilder().build();
    req.writeTo(out);
    DatagramPacket packet = new DatagramPacket(out.toByteArray(), out.size(),
        getBroadcastAddress(), DISCOVERY_PORT);
    socket.send(packet);
  }

  /**
   * Calculate the broadcast IP we need to send the packet along. If we send it
   * to 255.255.255.255, it never gets sent. I guess this has something to do
   * with the mobile network not wanting to do broadcast.
   */
  private InetAddress getBroadcastAddress() throws IOException {
    DhcpInfo dhcp = mWifi.getDhcpInfo();
    if (dhcp == null) {
      Log.d(TAG, "Could not get dhcp info");
      return null;
    }

    int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask;
    byte[] quads = new byte[4];
    for (int k = 0; k < 4; k++)
      quads[k] = (byte) ((broadcast >> k * 8) & 0xFF);
    return InetAddress.getByAddress(quads);
  }

  /**
   * Listen on socket for responses, timing out after TIMEOUT_MS
   * 
   * @param socket
   *          socket on which the announcement request was sent
   * @return list of discovered servers, never null
   * @throws IOException
   */
  private ArrayList<AnnounceResponse> listenForResponses(DatagramSocket socket)
  throws IOException {
    byte[] buf = new byte[1024];
    ArrayList<AnnounceResponse> servers = new ArrayList<AnnounceResponse>();

    // Loop and try to receive responses until the timeout elapses. We'll get
    // back the packet we just sent out, which isn't terribly helpful, but we'll
    // discard it in parseResponse because the cmd is wrong.
    try {
      while (true) {
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        socket.receive(packet);
        AnnounceResponse server = parseResponse(packet);
        if (server != null)
          servers.add(server);
      }
    } catch (SocketTimeoutException e) {
      Log.d(TAG, "Receive timed out");
    }
    return servers;
  }
  
  private boolean hasResponsePrefix(byte [] data) {
    if (data.length < VALID_RESPONSE.length)
      return false;
    for (int k = 0; k < VALID_RESPONSE.length; ++k)
      if (data[k] != VALID_RESPONSE[k])
        return false;
    return true;
  }

  private AnnounceResponse parseResponse(DatagramPacket packet) {
    byte[] bytes = packet.getData();
    if (!hasResponsePrefix(bytes))
      return null;
    
    // Copy everything after VALID_RESPONSE into an array so we
    // can call parseFrom. There must be a better way.
    byte[] proto = new byte[packet.getLength() - VALID_RESPONSE.length];
    for (int k = 0; k < proto.length; ++k)
      proto[k] = bytes[k + VALID_RESPONSE.length];
    
    try {
      AnnounceResponse resp = AnnounceResponse.parseFrom(proto);
      // Set the ip address we received the broadcast from.
      Log.d(TAG, String.format("Port is %d", resp.getPort()));
      InetAddress addr = ((InetSocketAddress) packet.getSocketAddress()).getAddress();
      return resp.toBuilder().setAddress(addr.getHostAddress()).build();
    } catch (InvalidProtocolBufferException e1) {
      Log.d(TAG, e1.toString());
      return null;
    }
  }
}